package com.example.user.service.code;

import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.RandomUtil;
import com.example.framework.common.exception.ServiceException;
import com.example.framework.common.factory.FactoryConcrete;
import com.example.framework.redis.core.util.RedisUtils;
import com.example.module.system.dal.redis.RedisKeyConstants;
import com.example.user.config.CodeFrequency;
import com.example.user.constants.ErrorCodeConstants;
import com.example.user.constants.SourceTypeEnum;
import com.example.user.controller.auth.vo.UserSourceType;

import java.time.*;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public abstract class CodeService implements FactoryConcrete<SourceTypeEnum> {

    private static String getHoursLimitKey(String redisKey) {
        return RedisKeyConstants.HOURLY_FREQUENCY_PREFIX + redisKey;
    }

    private static String getDailyLimitKey(String redisKey) {
        return String.format("%s%s:%s", RedisKeyConstants.DAILY_FREQUENCY_PREFIX, LocalDate.now(), redisKey);
    }

    private static String getCodeKey(String redisKey) {
        return RedisKeyConstants.CODE_PREFIX + redisKey;
    }

    protected String checkFrequencyAndGenerateCode(CodeFrequency codeFrequency, int codeLength, String id, int expireSeconds) {
        String redisKey = getRedisKey(id);

        // 5分钟有效期的话，允许1分钟后重新发送
        if (expireSeconds - RedisUtils.getExpire(getCodeKey(redisKey), TimeUnit.SECONDS) <= 60) { // getExpire() 不存在的key返回-2
            throw ServiceException.of(ErrorCodeConstants.TOO_MANY_CODE_REQUESTS);
        }

        // 1. 滑动时间窗口规则，n小时内最多发送x次
        if (hasHoursFrequencyLimit(codeFrequency)) {
            checkHoursFrequencyLimit(getHoursLimitKey(redisKey), codeFrequency.getLimitHours(), codeFrequency.getLimitCount());
        }

        // 2. 每日发送次数限制
        if (hasDailyFrequencyLimit(codeFrequency)) {
            checkDailyFrequencyLimit(getDailyLimitKey(redisKey), codeFrequency.getLimitCountPerDay());
        }

        return RandomUtil.randomNumbers(codeLength);
    }

    private boolean hasHoursFrequencyLimit(CodeFrequency codeFrequency) {
        if (Objects.isNull(codeFrequency) || !codeFrequency.getEnabled()) {
            return false;
        }
        Integer frequencyLimitHours = codeFrequency.getLimitHours();
        Integer frequencyLimitCount = codeFrequency.getLimitCount();
        return Objects.nonNull(frequencyLimitHours) && frequencyLimitHours > 0 && Objects.nonNull(frequencyLimitCount) && frequencyLimitCount > 0;
    }

    private boolean hasDailyFrequencyLimit(CodeFrequency codeFrequency) {
        if (Objects.isNull(codeFrequency) || !codeFrequency.getEnabled()) {
            return false;
        }
        Integer frequencyLimitCountPerDay = codeFrequency.getLimitCountPerDay();
        return Objects.nonNull(frequencyLimitCountPerDay) && frequencyLimitCountPerDay > 0;
    }

    private void checkHoursFrequencyLimit(String redisKey, int frequencyLimitHours, int frequencyLimitCount) {
        long hoursAgo = Instant.now().minus(frequencyLimitHours, ChronoUnit.HOURS).getEpochSecond();
        // 清理n小时前的数据，避免zset内存占用过大
        RedisUtils.zRemoveByScore(redisKey, 0, hoursAgo);
        Set<String> timestamps = RedisUtils.zRangeByScore(redisKey, hoursAgo, Instant.now().getEpochSecond());
        if (timestamps.size() >= frequencyLimitCount) {
            Long earliestTimestamps = timestamps.stream().map(Long::parseLong).min(Long::compareTo).get();
            throw ServiceException.of(ErrorCodeConstants.CODE_LIMITED, LocalDateTimeUtil.format(LocalDateTimeUtil.of(earliestTimestamps * 1000).plusHours(frequencyLimitHours), "HH:mm"));
        }
    }

    private void checkDailyFrequencyLimit(String redisKey, int frequencyLimitCountPerDay) {
        // 获取当天的发送次数
        Integer dailyCount = RedisUtils.get(redisKey, Integer.class);
        if (Objects.isNull(dailyCount)) {
            // 今天剩余的秒数
            LocalDateTime tomorrowMidnight = LocalDateTime.of(LocalDate.now().plusDays(1), LocalTime.MIDNIGHT);
            long todaySecondsLeft = Duration.between(LocalDateTime.now(), tomorrowMidnight).getSeconds();
            RedisUtils.set(redisKey, 0, todaySecondsLeft, TimeUnit.SECONDS);
        } else if (dailyCount >= frequencyLimitCountPerDay) {
            throw ServiceException.of(ErrorCodeConstants.CODE_DAILY_LIMITED);
        }
    }

    protected void setCache(CodeFrequency codeFrequency, String id, String identifyingCode, int expireSeconds) {
        String redisKey = getRedisKey(id);

        // 保存验证码
        RedisUtils.set(getCodeKey(redisKey), identifyingCode, expireSeconds, TimeUnit.SECONDS);
        // 记录滑动窗口
        if (hasHoursFrequencyLimit(codeFrequency)) {
            RedisUtils.zAdd(getHoursLimitKey(redisKey), Instant.now().getEpochSecond(), Instant.now().getEpochSecond());
            // 每次都续期
            RedisUtils.expire(getHoursLimitKey(redisKey), codeFrequency.getLimitHours(), TimeUnit.HOURS);
        }
        // 记录每日发送次数
        if (hasDailyFrequencyLimit(codeFrequency)) {
            // 增加当天的发送次数
            RedisUtils.increment(getDailyLimitKey(redisKey), 1);
        }
    }

    public abstract String getRedisKey(String id);

    public abstract void sendCode(UserSourceType reqVo);

    public void verifyCode(String id, String code, boolean needDeleteIfSuccess) {
        String redisKey = getRedisKey(id);

        String cachedCode = RedisUtils.getString(getCodeKey(redisKey));
        if (Objects.isNull(cachedCode)) {
            throw ServiceException.warn(ErrorCodeConstants.CODE_EXPIRED);
        }
        if (!cachedCode.equals(code)) {
            throw ServiceException.of(ErrorCodeConstants.CODE_ERROR);
        }
        if (needDeleteIfSuccess) { // 校验通过才能删除验证码
            RedisUtils.delete(getCodeKey(redisKey));
        }
    }
}
